<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<meta name="theme-color" content="#222"><meta name="generator" content="Hexo 6.3.0">

  <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png">
  <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32-next.png">
  <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16-next.png">
  <link rel="mask-icon" href="/images/logo.svg" color="#222">

<link rel="stylesheet" href="/css/main.css">



<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" integrity="sha256-Z1K5uhUaJXA7Ll0XrZ/0JhX4lAtZFpT6jkKrEDT0drU=" crossorigin="anonymous">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.1.1/animate.min.css" integrity="sha256-PR7ttpcvz8qrF57fur/yAx1qXMFJeJFiA6pSzWi0OIE=" crossorigin="anonymous">

<script class="next-config" data-name="main" type="application/json">{"hostname":"example.com","root":"/","images":"/images","scheme":"Muse","darkmode":false,"version":"8.14.1","exturl":false,"sidebar":{"position":"left","display":"post","padding":18,"offset":12},"copycode":{"enable":false,"style":null},"bookmark":{"enable":false,"color":"#222","save":"auto"},"mediumzoom":false,"lazyload":false,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":false,"nav":null},"stickytabs":false,"motion":{"enable":true,"async":false,"transition":{"menu_item":"fadeInDown","post_block":"fadeIn","post_header":"fadeInDown","post_body":"fadeInDown","coll_header":"fadeInLeft","sidebar":"fadeInUp"}},"prism":false,"i18n":{"placeholder":"搜索...","empty":"没有找到任何搜索结果：${query}","hits_time":"找到 ${hits} 个搜索结果（用时 ${time} 毫秒）","hits":"找到 ${hits} 个搜索结果"},"path":"/search.xml","localsearch":{"enable":true,"trigger":"auto","top_n_per_article":-1,"unescape":false,"preload":false}}</script><script src="/js/config.js"></script>

    <meta name="description" content="分布式锁简介 分布式锁，是一种思想，它的实现方式有很多。比如，我们将沙滩当做分布式锁的组件，那么它看起来应该是这样的：  加锁在沙滩上踩一脚，留下自己的脚印，就对应了加锁操作。其他进程或者线程，看到沙滩上已经有脚印，证明锁已被别人持有，则等待。 解锁把脚印从沙滩上抹去，就是解锁的过程。 锁超时为了避免死锁，我们可以设置一阵风，在单位时间后刮起，将脚印自动抹去。   分布式锁的实现有很多，比如基于数">
<meta property="og:type" content="article">
<meta property="og:title" content="Redis实现分布式锁">
<meta property="og:url" content="http://example.com/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/index.html">
<meta property="og:site_name" content="JsyBlog">
<meta property="og:description" content="分布式锁简介 分布式锁，是一种思想，它的实现方式有很多。比如，我们将沙滩当做分布式锁的组件，那么它看起来应该是这样的：  加锁在沙滩上踩一脚，留下自己的脚印，就对应了加锁操作。其他进程或者线程，看到沙滩上已经有脚印，证明锁已被别人持有，则等待。 解锁把脚印从沙滩上抹去，就是解锁的过程。 锁超时为了避免死锁，我们可以设置一阵风，在单位时间后刮起，将脚印自动抹去。   分布式锁的实现有很多，比如基于数">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="http://example.com/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/rlock.jpeg">
<meta property="og:image" content="http://example.com/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/redission-lock.jpeg">
<meta property="og:image" content="http://example.com/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/lock1.webp">
<meta property="og:image" content="http://example.com/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/lock2.webp">
<meta property="og:image" content="http://example.com/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/unlock.webp">
<meta property="article:published_time" content="2021-11-29T03:00:11.000Z">
<meta property="article:modified_time" content="2022-09-02T14:44:06.077Z">
<meta property="article:author" content="SongyangJi">
<meta property="article:tag" content="NoSQL">
<meta property="article:tag" content="Redis">
<meta property="article:tag" content="分布式锁">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="http://example.com/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/rlock.jpeg">


<link rel="canonical" href="http://example.com/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/">



<script class="next-config" data-name="page" type="application/json">{"sidebar":"","isHome":false,"isPost":true,"lang":"zh-CN","comments":true,"permalink":"http://example.com/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/","path":"2021/11/29/Redis实现分布式锁/","title":"Redis实现分布式锁"}</script>

<script class="next-config" data-name="calendar" type="application/json">""</script>
<title>Redis实现分布式锁 | JsyBlog</title>
  








  <noscript>
    <link rel="stylesheet" href="/css/noscript.css">
  </noscript>
</head>

<body itemscope itemtype="http://schema.org/WebPage" class="use-motion">
  <div class="headband"></div>

  <main class="main">
    <div class="column">
      <header class="header" itemscope itemtype="http://schema.org/WPHeader"><div class="site-brand-container">
  <div class="site-nav-toggle">
    <div class="toggle" aria-label="切换导航栏" role="button">
        <span class="toggle-line"></span>
        <span class="toggle-line"></span>
        <span class="toggle-line"></span>
    </div>
  </div>

  <div class="site-meta">

    <a href="/" class="brand" rel="start">
      <i class="logo-line"></i>
      <p class="site-title">JsyBlog</p>
      <i class="logo-line"></i>
    </a>
  </div>

  <div class="site-nav-right">
    <div class="toggle popup-trigger" aria-label="搜索" role="button">
        <i class="fa fa-search fa-fw fa-lg"></i>
    </div>
  </div>
</div>



<nav class="site-nav">
  <ul class="main-menu menu"><li class="menu-item menu-item-home"><a href="/" rel="section"><i class="fa fa-home fa-fw"></i>首页</a></li><li class="menu-item menu-item-tags"><a href="/tags/" rel="section"><i class="fa fa-tags fa-fw"></i>标签</a></li><li class="menu-item menu-item-categories"><a href="/categories/" rel="section"><i class="fa fa-th fa-fw"></i>分类</a></li><li class="menu-item menu-item-archives"><a href="/archives/" rel="section"><i class="fa fa-archive fa-fw"></i>归档</a></li>
      <li class="menu-item menu-item-search">
        <a role="button" class="popup-trigger"><i class="fa fa-search fa-fw"></i>搜索
        </a>
      </li>
  </ul>
</nav>



  <div class="search-pop-overlay">
    <div class="popup search-popup"><div class="search-header">
  <span class="search-icon">
    <i class="fa fa-search"></i>
  </span>
  <div class="search-input-container">
    <input autocomplete="off" autocapitalize="off" maxlength="80"
           placeholder="搜索..." spellcheck="false"
           type="search" class="search-input">
  </div>
  <span class="popup-btn-close" role="button">
    <i class="fa fa-times-circle"></i>
  </span>
</div>
<div class="search-result-container no-result">
  <div class="search-result-icon">
    <i class="fa fa-spinner fa-pulse fa-5x"></i>
  </div>
</div>

    </div>
  </div>

</header>
        
  
  <aside class="sidebar">

    <div class="sidebar-inner sidebar-nav-active sidebar-toc-active">
      <ul class="sidebar-nav">
        <li class="sidebar-nav-toc">
          文章目录
        </li>
        <li class="sidebar-nav-overview">
          站点概览
        </li>
      </ul>

      <div class="sidebar-panel-container">
        <!--noindex-->
        <div class="post-toc-wrap sidebar-panel">
            <div class="post-toc animated"><ol class="nav"><li class="nav-item nav-level-1"><a class="nav-link" href="#%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E7%AE%80%E4%BB%8B"><span class="nav-number">1.</span> <span class="nav-text">分布式锁简介</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#%E9%94%81%E7%9A%84%E5%AE%89%E5%85%A8%E5%92%8C%E6%B4%BB%E6%80%A7%E4%BF%9D%E8%AF%81"><span class="nav-number">1.1.</span> <span class="nav-text">锁的安全和活性保证</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E4%B8%BB%E4%BB%8E%E7%BB%93%E6%9E%84%E6%95%85%E9%9A%9C%E8%BD%AC%E7%A7%BB%E6%97%B6%E7%9A%84%E5%B9%B6%E5%8F%91%E9%97%AE%E9%A2%98"><span class="nav-number">1.2.</span> <span class="nav-text">主从结构故障转移时的并发问题</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#%E5%8D%95%E6%9C%BA%E4%B8%8BRedis%E5%AE%9E%E7%8E%B0%E9%94%81"><span class="nav-number">2.</span> <span class="nav-text">单机下Redis实现锁</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#%E9%94%81%E7%9A%84%E8%8E%B7%E5%8F%96"><span class="nav-number">2.1.</span> <span class="nav-text">锁的获取</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E9%94%81%E7%9A%84%E9%87%8A%E6%94%BE"><span class="nav-number">2.2.</span> <span class="nav-text">锁的释放</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E4%BD%BF%E7%94%A8Jedis%E5%AE%9E%E7%8E%B0%E2%80%9D%E4%B8%90%E7%89%88%E2%80%9CRedis%E9%94%81"><span class="nav-number">2.3.</span> <span class="nav-text">使用Jedis实现”丐版“Redis锁</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#Redisson%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81"><span class="nav-number">3.</span> <span class="nav-text">Redisson实现分布式锁</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#%E4%BD%BF%E7%94%A8Redisson%E4%B8%AD%E7%9A%84%E9%94%81"><span class="nav-number">3.1.</span> <span class="nav-text">使用Redisson中的锁</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#Redisson%E4%B8%ADRLock%E7%9A%84%E5%AE%9E%E7%8E%B0%E7%89%B9%E7%82%B9"><span class="nav-number">3.2.</span> <span class="nav-text">Redisson中RLock的实现特点</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%8A%A0%E9%94%81%E6%95%B4%E4%BD%93%E6%B5%81%E7%A8%8B"><span class="nav-number">3.3.</span> <span class="nav-text">加锁整体流程</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#%E6%9E%84%E9%80%A0%E9%94%81"><span class="nav-number">3.3.1.</span> <span class="nav-text">构造锁</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#%E5%8A%A0%E9%94%81"><span class="nav-number">3.3.2.</span> <span class="nav-text">加锁</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#%E8%8E%B7%E5%8F%96%E4%BA%92%E6%96%A5%E7%8A%B6%E6%80%81"><span class="nav-number">3.3.3.</span> <span class="nav-text">获取互斥状态</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#%E5%BA%95%E5%B1%82%E5%8A%A0%E9%94%81%E9%80%BB%E8%BE%91"><span class="nav-number">3.3.3.1.</span> <span class="nav-text">底层加锁逻辑</span></a></li></ol></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E8%A7%A3%E9%94%81%E6%95%B4%E4%BD%93%E6%B5%81%E7%A8%8B"><span class="nav-number">3.4.</span> <span class="nav-text">解锁整体流程</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#%E8%A7%A3%E9%94%81"><span class="nav-number">3.4.1.</span> <span class="nav-text">解锁</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#%E8%A7%A3%E9%94%81%E7%9A%84%E5%BA%95%E5%B1%82%E9%80%BB%E8%BE%91"><span class="nav-number">3.4.2.</span> <span class="nav-text">解锁的底层逻辑</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#Redisson%E7%9A%84%E9%94%81%E6%80%BB%E7%BB%93"><span class="nav-number">3.5.</span> <span class="nav-text">Redisson的锁总结</span></a></li></ol></li></ol></div>
        </div>
        <!--/noindex-->

        <div class="site-overview-wrap sidebar-panel">
          <div class="site-author animated" itemprop="author" itemscope itemtype="http://schema.org/Person">
  <p class="site-author-name" itemprop="name">SongyangJi</p>
  <div class="site-description" itemprop="description"></div>
</div>
<div class="site-state-wrap animated">
  <nav class="site-state">
      <div class="site-state-item site-state-posts">
        <a href="/archives/">
          <span class="site-state-item-count">251</span>
          <span class="site-state-item-name">日志</span>
        </a>
      </div>
      <div class="site-state-item site-state-categories">
          <a href="/categories/">
        <span class="site-state-item-count">45</span>
        <span class="site-state-item-name">分类</span></a>
      </div>
      <div class="site-state-item site-state-tags">
          <a href="/tags/">
        <span class="site-state-item-count">109</span>
        <span class="site-state-item-name">标签</span></a>
      </div>
  </nav>
</div>

        </div>
      </div>
    </div>

    
  </aside>


    </div>

    <div class="main-inner post posts-expand">


  


<div class="post-block">
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-content" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/avatar.gif">
      <meta itemprop="name" content="SongyangJi">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="JsyBlog">
      <meta itemprop="description" content="">
    </span>

    <span hidden itemprop="post" itemscope itemtype="http://schema.org/CreativeWork">
      <meta itemprop="name" content="Redis实现分布式锁 | JsyBlog">
      <meta itemprop="description" content="">
    </span>
      <header class="post-header">
        <h1 class="post-title" itemprop="name headline">
          Redis实现分布式锁
        </h1>

        <div class="post-meta-container">
          <div class="post-meta">
    <span class="post-meta-item">
      <span class="post-meta-item-icon">
        <i class="far fa-calendar"></i>
      </span>
      <span class="post-meta-item-text">发表于</span>

      <time title="创建时间：2021-11-29 11:00:11" itemprop="dateCreated datePublished" datetime="2021-11-29T11:00:11+08:00">2021-11-29</time>
    </span>
    <span class="post-meta-item">
      <span class="post-meta-item-icon">
        <i class="far fa-calendar-check"></i>
      </span>
      <span class="post-meta-item-text">更新于</span>
      <time title="修改时间：2022-09-02 22:44:06" itemprop="dateModified" datetime="2022-09-02T22:44:06+08:00">2022-09-02</time>
    </span>
    <span class="post-meta-item">
      <span class="post-meta-item-icon">
        <i class="far fa-folder"></i>
      </span>
      <span class="post-meta-item-text">分类于</span>
        <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
          <a href="/categories/Redis/" itemprop="url" rel="index"><span itemprop="name">Redis</span></a>
        </span>
    </span>

  
</div>

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">
        <h1 id="分布式锁简介"><a href="#分布式锁简介" class="headerlink" title="分布式锁简介"></a>分布式锁简介</h1><blockquote>
<p>分布式锁，是一种思想，它的实现方式有很多。比如，我们将沙滩当做分布式锁的组件，那么它看起来应该是这样的：</p>
<ul>
<li><strong>加锁</strong><br>在沙滩上踩一脚，留下自己的脚印，就对应了加锁操作。其他进程或者线程，看到沙滩上已经有脚印，证明锁已被别人持有，则等待。</li>
<li><strong>解锁</strong><br>把脚印从沙滩上抹去，就是解锁的过程。</li>
<li><strong>锁超时</strong><br>为了避免死锁，我们可以设置一阵风，在单位时间后刮起，将脚印自动抹去。</li>
</ul>
</blockquote>
<p>分布式锁的实现有很多，比如基于数据库、memcached、Redis、系统文件、zookeeper等。它们的核心的理念跟上面的过程大致相同。</p>
<p>在许多环境中不同进程必须以互斥方式使用共享资源进行操作时，分布式锁是一个非常有用的原语。此试图提供一种更规范的算法来实现Redis的分布式锁。我们提出了一种称为<code>Redlock</code>的算法，它实现了<code>DLM</code>（猜测是<code>Distributed Lock Manager</code>的缩写，分布式锁管理器），我们认为它比普通的单实例方法更安全。</p>
<h2 id="锁的安全和活性保证"><a href="#锁的安全和活性保证" class="headerlink" title="锁的安全和活性保证"></a>锁的安全和活性保证</h2><p>算法的三个核心特征（三大最低保证）：</p>
<ul>
<li><strong>Safety property</strong>（安全性）：互斥。确保在任何给定时刻下，只有一个客户端可以持有锁；</li>
<li><strong>Liveness property A</strong>（活性A）：无死锁。即使存在曾经锁定资源的客户端崩溃或者出现网络分区异常，确保锁总是能够成功获取；</li>
<li><strong>Liveness property B</strong>（活性B）：容错性。只要大多数Redis节点处于正常运行状态，客户端就可以获取和释放锁。</li>
</ul>
<h2 id="主从结构故障转移时的并发问题"><a href="#主从结构故障转移时的并发问题" class="headerlink" title="主从结构故障转移时的并发问题"></a>主从结构故障转移时的并发问题</h2><p>文档中还指出了目前算法对于故障转移的实现还存在明显的竞态条件问题（描述的应该是<code>Redis</code>主从架构下的问题）：</p>
<ul>
<li>客户端A获取Redis主节点中的锁（假设锁定的资源为X）</li>
<li>在Redis主节点把KEY同步到Redis从节点之前，Redis主节点崩溃</li>
<li>Redis从节点因为故障晋升为主节点</li>
<li>此时，客户端B获取资源X的锁成功，问题是资源X的锁在前面已经被客户端A获取过，这样就出现了并发问题。</li>
</ul>
<h1 id="单机下Redis实现锁"><a href="#单机下Redis实现锁" class="headerlink" title="单机下Redis实现锁"></a>单机下Redis实现锁</h1><p>单个实例是我们将用于此处描述的分布式算法的基础。</p>
<h2 id="锁的获取"><a href="#锁的获取" class="headerlink" title="锁的获取"></a>锁的获取</h2><p>下面的redis命令是获取锁的核心思想 —— 如果键不存在，则设置一个id值（此时相当于拿到了锁），并且为了保证上面所说的活性要求，还设置了键的过期时间。</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SET resource_name my_random_value NX PX 30000</span><br></pre></td></tr></table></figure>

<p>这实际上是<code>setnx</code>和<code>setex</code>的结合体，不过它是原子的。</p>
<p>如果较低版本的客户端中不支持这个命令，那就要使用lua脚本。</p>
<h2 id="锁的释放"><a href="#锁的释放" class="headerlink" title="锁的释放"></a>锁的释放</h2><p>下面的lua脚本的含义是：</p>
<p>当键存在，并且键值正是当前线程的独属id时（也就是防止释放别的线程加的锁，这就是这个独属于加到锁的线程的id的作用）。</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> redis.call(<span class="string">&quot;get&quot;</span>,KEYS[<span class="number">1</span>]) == ARGV[<span class="number">1</span>] <span class="keyword">then</span></span><br><span class="line">    <span class="keyword">return</span> redis.call(<span class="string">&quot;del&quot;</span>,KEYS[<span class="number">1</span>])</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>




<h2 id="使用Jedis实现”丐版“Redis锁"><a href="#使用Jedis实现”丐版“Redis锁" class="headerlink" title="使用Jedis实现”丐版“Redis锁"></a>使用Jedis实现”丐版“Redis锁</h2><p><del>注意下面的代码只能算是对redis加锁的原理予以初步的实践，远远达不到高性能、高可靠。</del></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyRedisLock</span> <span class="keyword">implements</span> <span class="title class_">Lock</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">JedisPool</span> <span class="variable">jedisPool</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JedisPool</span>(<span class="string">&quot;localhost&quot;</span>);</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">EXPIRE_TIME_OUT</span> <span class="operator">=</span> <span class="number">1000</span>;</span><br><span class="line">    <span class="comment">// SET命令已经提供了可选的复合操作符</span></span><br><span class="line">    <span class="comment">// EX：设置超时时间，单位是秒</span></span><br><span class="line">    <span class="comment">// PX：设置超时时间，单位是毫秒</span></span><br><span class="line">    <span class="comment">// NX：IF NOT EXIST的缩写，只有KEY不存在的前提下才会设置K-V，设置成功返回1，否则返回0</span></span><br><span class="line">    <span class="comment">// XX：IF EXIST的缩写，只有在KEY存在的前提下才会设置K-V，设置成功返回1，否则返回0</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">SetParams</span> <span class="variable">params</span> <span class="operator">=</span> SetParams.setParams().nx().px(EXPIRE_TIME_OUT);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ThreadLocal&lt;String&gt; lockedId = <span class="keyword">new</span> <span class="title class_">ThreadLocal</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String lockName;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="title function_">MyRedisLock</span><span class="params">(String lockName)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.lockName = lockName;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> MyRedisLock <span class="title function_">getLock</span><span class="params">(String lockName)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">MyRedisLock</span>(lockName);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="meta">@SneakyThrows</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">lock</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">id</span> <span class="operator">=</span> UUID.randomUUID().toString();</span><br><span class="line">        lockedId.set(id);</span><br><span class="line">        <span class="keyword">try</span> (<span class="type">Jedis</span> <span class="variable">jedis</span> <span class="operator">=</span> jedisPool.getResource()) &#123;</span><br><span class="line">            <span class="comment">// 加锁失败就重试</span></span><br><span class="line">            <span class="keyword">for</span> (; ; ) &#123;</span><br><span class="line">                <span class="type">String</span> <span class="variable">statusCode</span> <span class="operator">=</span> jedis.set(lockName, id, params);</span><br><span class="line">                <span class="keyword">if</span> (<span class="string">&quot;OK&quot;</span>.equals(statusCode)) &#123;</span><br><span class="line">                    <span class="keyword">return</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                Thread.sleep(<span class="number">100</span>); <span class="comment">// 可以设计成 wait-notify 范式，不过这里就简陋一点了</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> (<span class="type">Jedis</span> <span class="variable">jedis</span> <span class="operator">=</span> jedisPool.getResource()) &#123;</span><br><span class="line">            <span class="comment">// 为做到原子性，使用 Lua 脚本</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">script</span> <span class="operator">=</span></span><br><span class="line">                    <span class="string">&quot;if redis.call(&#x27;get&#x27;,KEYS[1]) == ARGV[1] then&quot;</span> +</span><br><span class="line">                            <span class="string">&quot;   return redis.call(&#x27;del&#x27;,KEYS[1]) &quot;</span> +</span><br><span class="line">                            <span class="string">&quot;else&quot;</span> +</span><br><span class="line">                            <span class="string">&quot;   return 0 &quot;</span> +</span><br><span class="line">                            <span class="string">&quot;end&quot;</span>;</span><br><span class="line">            <span class="type">String</span> <span class="variable">id</span> <span class="operator">=</span> lockedId.get();</span><br><span class="line">            jedis.eval(script, Collections.singletonList(lockName), Collections.singletonList(id)).toString();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  </span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">lockInterruptibly</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(<span class="type">long</span> time, TimeUnit unit)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Condition <span class="title function_">newCondition</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">		<span class="comment">// 测试代码</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">MyRedisLock</span> <span class="variable">lock</span> <span class="operator">=</span> MyRedisLock.getLock(<span class="string">&quot;lock1&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">//        MyInteger.testLock(lock);</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="type">long</span> <span class="variable">l</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        <span class="keyword">final</span> <span class="type">int</span> <span class="variable">N</span> <span class="operator">=</span> <span class="number">10000</span>;</span><br><span class="line">        <span class="type">MyInteger</span> <span class="variable">integer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MyInteger</span>(N);</span><br><span class="line">        <span class="type">CountDownLatch</span> <span class="variable">countDownLatch</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CountDownLatch</span>(N);</span><br><span class="line">        <span class="type">ExecutorService</span> <span class="variable">pool</span> <span class="operator">=</span> Executors.newFixedThreadPool(<span class="number">10</span>);</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; N; i++) &#123;</span><br><span class="line">            pool.submit(() -&gt; &#123;</span><br><span class="line">                lock.lock();</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    integer.decr();</span><br><span class="line">                &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                    lock.unlock();</span><br><span class="line">                &#125;</span><br><span class="line">                countDownLatch.countDown();</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            countDownLatch.await(); <span class="comment">// just wait all tasks to finish</span></span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">        System.out.println(<span class="string">&quot;end : &quot;</span> + integer.getX());</span><br><span class="line">        <span class="type">long</span> <span class="variable">r</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        System.out.println(<span class="string">&quot;耗时 &quot;</span> + (r - l) + <span class="string">&quot; ms&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>


<p>上述算法的主要缺点：</p>
<ol>
<li>忙等（busy-waiting）的问题，消耗CPU资源；</li>
<li>只适用于单机；</li>
<li>不可重入</li>
<li>没有限制等待获取的锁的超时时间</li>
<li>不能做到在等待锁的时候响应中断</li>
</ol>
<h1 id="Redisson实现分布式锁"><a href="#Redisson实现分布式锁" class="headerlink" title="Redisson实现分布式锁"></a>Redisson实现分布式锁</h1><p><a target="_blank" rel="noopener" href="https://redisson.org/">Redisson</a>是架设在<a target="_blank" rel="noopener" href="http://redis.cn/">Redis</a>基础上的一个Java驻内存数据网格（In-Memory Data Grid）。</p>
<p>相对于Jedis而言，Redisson是一个更高级的客户端。</p>
<p>它里面实现了各种分布式锁（本文只介绍和使用简单的）。</p>
<h2 id="使用Redisson中的锁"><a href="#使用Redisson中的锁" class="headerlink" title="使用Redisson中的锁"></a>使用Redisson中的锁</h2><p><strong>maven依赖</strong></p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.redisson<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>redisson<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.16.4<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>


<p><strong>简单使用 lock</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">  <span class="comment">// 配置类</span></span><br><span class="line">  <span class="type">Config</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Config</span>();</span><br><span class="line">  config.useSingleServer().setAddress(<span class="string">&quot;redis://127.0.0.1:6379&quot;</span>);</span><br><span class="line">  config.useSingleServer().setPassword(<span class="string">&quot;redis1234&quot;</span>);</span><br><span class="line">  <span class="comment">// 初始化客户端</span></span><br><span class="line">  <span class="keyword">final</span> <span class="type">RedissonClient</span> <span class="variable">client</span> <span class="operator">=</span> Redisson.create(config);  </span><br><span class="line">  <span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> client.getLock(<span class="string">&quot;lock1&quot;</span>);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 加锁</span></span><br><span class="line">lock.lock();</span><br><span class="line">  <span class="keyword">try</span>&#123;</span><br><span class="line">      <span class="comment">// do something ...  </span></span><br><span class="line">  &#125;<span class="keyword">finally</span>&#123;</span><br><span class="line">      <span class="comment">// 释放锁</span></span><br><span class="line">      lock.unlock();</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure>



<h2 id="Redisson中RLock的实现特点"><a href="#Redisson中RLock的实现特点" class="headerlink" title="Redisson中RLock的实现特点"></a>Redisson中RLock的实现特点</h2><p><code>Redisson</code>中<code>RLock</code>的实现是基本参照了<code>Redis</code>的<code>red lock</code>算法进行实现，不过在原始的<code>red lock</code>算法下进行了改良，主要包括下面的特性：</p>
<ul>
<li>互斥</li>
<li>无死锁</li>
<li>可重入，类似于<code>ReentrantLock</code>，同一个线程可以重复获取同一个资源的锁（一般使用计数器实现），锁的重入特性一般情况下有利于提高资源的利用率</li>
<li><strong>续期</strong>，这个是一个比较前卫解决思路，也就是如果一个客户端对资源<code>X</code>永久锁定，那么并不是直接对<code>KEY</code>生存周期设置为<code>-1</code>，而是通<strong>过一个守护线程每隔固定周期延长<code>KEY</code>的过期时间</strong>，这样就能实现<strong>在守护线程不被杀掉的前提下，避免客户端崩溃导致锁无法释放长期占用资源的问题</strong></li>
<li><strong>锁状态变更订阅</strong>，依赖于<code>org.redisson.pubsub.LockPubSub</code>，用于订阅和通知锁释放事件</li>
<li>数据类型选用了<code>HASH</code>，配合<code>Lua</code>脚本完成多个命令的原子性</li>
</ul>
<p><code>RLock</code>的类图</p>
<p><img src="/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/rlock.jpeg"></p>
<p>RedissonLock<code>就是</code>RLock<code>的直接实现，也是分布式锁实现的核心类，从源码中看到</code>Redisson.getLock()<code>就是直接实例化</code>RedissonLock。</p>
<p><code>RedissonLock</code>的类继承图</p>
<p><img src="/2021/11/29/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/redission-lock.jpeg"></p>
<h2 id="加锁整体流程"><a href="#加锁整体流程" class="headerlink" title="加锁整体流程"></a>加锁整体流程</h2><blockquote>
<p>下面的代码是版本3.16.4中的，其他版本有点差别：</p>
</blockquote>
<h3 id="构造锁"><a href="#构造锁" class="headerlink" title="构造锁"></a>构造锁</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="title function_">RedissonLock</span><span class="params">(CommandAsyncExecutor commandExecutor, String name)</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>(commandExecutor, name);</span><br><span class="line">    <span class="comment">//命令执行器</span></span><br><span class="line">    <span class="built_in">this</span>.commandExecutor = commandExecutor;</span><br><span class="line">    <span class="comment">//UUID字符串</span></span><br><span class="line">    <span class="built_in">this</span>.id = commandExecutor.getConnectionManager().getId();</span><br><span class="line">    <span class="comment">//内部锁过期时间(默认是 30*1000 , 也就是 30 秒)</span></span><br><span class="line">    <span class="built_in">this</span>.internalLockLeaseTime = commandExecutor.</span><br><span class="line">                getConnectionManager().getCfg().getLockWatchdogTimeout();</span><br><span class="line">    <span class="built_in">this</span>.entryName = id + <span class="string">&quot;:&quot;</span> + name;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><code>lockWatchdogTimeout</code>参数只有在没有使用<code>leaseTimeout</code>参数定义的成功获取到锁的场景（简单来说就是不设置时限的加锁）下生效，<br>如果看门狗在下一个<code>lockWatchdogTimeout</code>周期内不进行续期，那么锁就会过期释放<br>（从源码上看，每三分之一<code>lockWatchdogTimeout</code>就会执行一次续期任务，每次通过<code>pexpire</code>把<code>KEY</code>的存活周期延长<code>lockWatchdogTimeout</code>），<code>lockWatchdogTimeout</code>的默认值为<code>30000</code>，也就是<code>30</code>秒。</p>
<p>这里先列举一下<code>RedissonLock</code>中获取名称的方法，以便后面分析这些名称作为<code>K-V</code>结构的<code>KEY</code>时候使用：</p>
<ul>
<li><code>id</code>：由配置实例化时候实例化的<code>UUID</code>实例生成，从源码上分析每个连接方式的<code>Redisson</code>实例有唯一的<code>UUID</code>，<code>ConnectionManager</code>初始化的时候会调用<code>UUID id = UUID.randomUUID()</code>，笔者认为可以理解为<code>Redisson</code>实例在某个应用程序进程中的唯一标识，毕竟一般情况下，一个应用程序应该只会应用一种<code>Redisson</code>的连接方式</li>
<li><code>getEntryName()</code>：返回的是<code>UUID + : + $KEY</code>，例如<code>559cc9df-bad8-4f6c-86a4-ffa51b7f1c36:resource:x</code></li>
<li><code>getName()</code>：返回的是<code>$KEY</code>，例如<code>resource:x</code></li>
<li><code>getChannelName()</code>：返回的是<code>redisson_lock__channel:&#123;$KEY&#125;</code>，例如<code>redisson_lock__channel:&#123;resource:x&#125;</code></li>
<li><code>getLockName(long threadId)</code>：返回的是<code>UUID + : + $threadId</code>，例如<code>559cc9df-bad8-4f6c-86a4-ffa51b7f1c36:1</code></li>
</ul>
<h3 id="加锁"><a href="#加锁" class="headerlink" title="加锁"></a>加锁</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line">    <span class="comment">// 获取锁</span></span><br><span class="line">		<span class="keyword">public</span> RLock <span class="title function_">getLock</span><span class="params">(String name)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RedissonLock</span>(commandExecutor, name);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">		<span class="comment">/**</span></span><br><span class="line"><span class="comment">   	 * LeaseTime - 获取后持有锁的最长时间，如果它尚未通过调用unlock 。 如果leaseTime 为-1，则保持锁定直到显式解锁。</span></span><br><span class="line"><span class="comment">   	 */</span></span><br><span class="line">		 <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">lock</span><span class="params">(<span class="type">long</span> leaseTime, TimeUnit unit, <span class="type">boolean</span> interruptibly)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">      	<span class="comment">// 当前线程ID</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">threadId</span> <span class="operator">=</span> Thread.currentThread().getId();</span><br><span class="line">      	<span class="comment">// 尝试获取锁</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">ttl</span> <span class="operator">=</span> tryAcquire(-<span class="number">1</span>, leaseTime, unit, threadId);</span><br><span class="line">        <span class="comment">// lock acquired</span></span><br><span class="line">        <span class="comment">// 如果ttl为 null，则证明获取锁成功</span></span><br><span class="line">        <span class="keyword">if</span> (ttl == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">      	<span class="comment">// 如果获取锁失败，则订阅到对应这个锁的channel</span></span><br><span class="line">        RFuture&lt;RedissonLockEntry&gt; future = subscribe(threadId);</span><br><span class="line">        <span class="keyword">if</span> (interruptibly) &#123;</span><br><span class="line">            commandExecutor.syncSubscriptionInterrupted(future);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            commandExecutor.syncSubscription(future);</span><br><span class="line">        &#125;</span><br><span class="line">       </span><br><span class="line">        <span class="comment">// 不断尝试</span></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">                <span class="comment">// 再次尝试获取锁</span></span><br><span class="line">                ttl = tryAcquire(-<span class="number">1</span>, leaseTime, unit, threadId);</span><br><span class="line">                <span class="comment">// lock acquired</span></span><br><span class="line">                <span class="keyword">if</span> (ttl == <span class="literal">null</span>) &#123;</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                <span class="comment">// waiting for message</span></span><br><span class="line">                <span class="comment">// ttl大于0 则等待ttl时间后继续尝试获取</span></span><br><span class="line">                <span class="keyword">if</span> (ttl &gt;= <span class="number">0</span>) &#123;</span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                        <span class="keyword">if</span> (interruptibly) &#123;</span><br><span class="line">                            <span class="keyword">throw</span> e;</span><br><span class="line">                        &#125;</span><br><span class="line">                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="keyword">if</span> (interruptibly) &#123;</span><br><span class="line">                        future.getNow().getLatch().acquire();</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        future.getNow().getLatch().acquireUninterruptibly();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">// 取消对channel的订阅</span></span><br><span class="line">            unsubscribe(future, threadId);</span><br><span class="line">        &#125;</span><br><span class="line"><span class="comment">//        get(lockAsync(leaseTime, unit));</span></span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>



<p>如上代码，就是加锁的全过程。先调用<code>tryAcquire</code>来获取锁，如果返回值ttl为空，则证明加锁成功，返回；如果不为空，则证明加锁失败。这时候，它会订阅这个锁的Channel，等待锁释放的消息，然后重新尝试获取锁。流程如下：</p>
<img src="lock1.webp" style="zoom:60%;" />











<p>获取锁的过程是怎样的呢？接下来就要看<code>tryAcquire</code>方法。在这里，它有两种处理方式，一种是带有过期时间的锁，一种是不带过期时间的锁。</p>
<h3 id="获取互斥状态"><a href="#获取互斥状态" class="headerlink" title="获取互斥状态"></a>获取互斥状态</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">    <span class="comment">// 同步方法依赖异步方法</span></span><br><span class="line">    <span class="keyword">private</span> Long <span class="title function_">tryAcquire</span><span class="params">(<span class="type">long</span> waitTime, <span class="type">long</span> leaseTime, TimeUnit unit, <span class="type">long</span> threadId)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 通过传入锁持有的最大时间和线程ID异步获取锁</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> &lt;T&gt; RFuture&lt;Long&gt; <span class="title function_">tryAcquireAsync</span><span class="params">(<span class="type">long</span> waitTime, <span class="type">long</span> leaseTime, TimeUnit unit, <span class="type">long</span> threadId)</span> &#123;</span><br><span class="line">        RFuture&lt;Long&gt; ttlRemainingFuture;</span><br><span class="line">  </span><br><span class="line">  </span><br><span class="line">        <span class="keyword">if</span> (leaseTime != -<span class="number">1</span>) &#123;   <span class="comment">// 锁持有最大时间不为-1，也就是明确锁的持有时间，不是永久持有的场景</span></span><br><span class="line">            ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// 先按照30秒的过期时间来执行获取锁的方法</span></span><br><span class="line">            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,</span><br><span class="line">                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 如果还持有这个锁，则开启定时任务不断刷新该锁的过期时间</span></span><br><span class="line">        ttlRemainingFuture.onComplete((ttlRemaining, e) -&gt; &#123;</span><br><span class="line">            <span class="comment">// // 执行异常场景直接返回</span></span><br><span class="line">            <span class="keyword">if</span> (e != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// lock acquired（当前已经获取到锁）</span></span><br><span class="line">            <span class="keyword">if</span> (ttlRemaining == <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">if</span> (leaseTime != -<span class="number">1</span>) &#123;</span><br><span class="line">                    internalLockLeaseTime = unit.toMillis(leaseTime);</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="comment">// 如果是永久持有，定时调度进行 续期操作</span></span><br><span class="line">                    scheduleExpirationRenewal(threadId);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        <span class="keyword">return</span> ttlRemainingFuture;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>





<h4 id="底层加锁逻辑"><a href="#底层加锁逻辑" class="headerlink" title="底层加锁逻辑"></a>底层加锁逻辑</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">&lt;T&gt; RFuture&lt;T&gt; <span class="title function_">tryLockInnerAsync</span><span class="params">(<span class="type">long</span> waitTime, <span class="type">long</span> leaseTime, TimeUnit unit, <span class="type">long</span> threadId, RedisStrictCommand&lt;T&gt; command)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,</span><br><span class="line">             <span class="comment">//如果锁不存在，则通过hincrby设置持有数为 1 ，并设置过期时间</span></span><br><span class="line">             <span class="comment">// KEYS[1] ：hash类型的键，代表哪个锁</span></span><br><span class="line">             <span class="comment">// ARGV[2] ：hash类型的字段，代表目前获取到锁的线程名</span></span><br><span class="line">             <span class="comment">// ARGV[1] ：传入的锁的过期时间                       </span></span><br><span class="line">            <span class="string">&quot;if (redis.call(&#x27;exists&#x27;, KEYS[1]) == 0) then &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;redis.call(&#x27;hincrby&#x27;, KEYS[1], ARGV[2], 1); &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;redis.call(&#x27;pexpire&#x27;, KEYS[1], ARGV[1]); &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;return nil; &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;end; &quot;</span> +</span><br><span class="line">                    <span class="comment">// 如果锁已存在，并且锁的是当前线程，则通过hincrby给数值递增1</span></span><br><span class="line">                    <span class="string">&quot;if (redis.call(&#x27;hexists&#x27;, KEYS[1], ARGV[2]) == 1) then &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;redis.call(&#x27;hincrby&#x27;, KEYS[1], ARGV[2], 1); &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;redis.call(&#x27;pexpire&#x27;, KEYS[1], ARGV[1]); &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;return nil; &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;end; &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;return redis.call(&#x27;pttl&#x27;, KEYS[1]);&quot;</span>, <span class="comment">// //如果锁已存在，但并非本线程，则返回过期时间ttl</span></span><br><span class="line">            Collections.singletonList(getRawName(), unit.toMillis(leaseTime), getLockName(threadId));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>这段LUA代码看起来并不复杂，有三个判断：</p>
<ul>
<li><strong>通过exists判断，如果锁不存在，则设置值和过期时间，加锁成功</strong></li>
<li><strong>通过hexists判断，如果锁已存在，并且锁的是当前线程，则证明是重入锁，加锁成功</strong></li>
<li><strong>如果锁已存在，但锁的不是当前线程，则证明有其他线程持有锁。返回当前锁的过期时间，加锁失败</strong></li>
</ul>
<img src="lock2.webp" style="zoom:60%;" />





<p>最后再概括一下<code>Redisson</code>中实现<code>red lock</code>算法使用的<code>HASH</code>数据类型：</p>
<ul>
<li><code>KEY</code>代表的就是资源或者锁，<strong>创建、存在性判断，延长生存周期和删除操作总是针对<code>KEY</code>进行的</strong></li>
<li><code>FIELD</code>代表的是锁名称<code>lockName()</code>，但是其实它由<code>Redisson</code>连接管理器实例的初始化<code>UUID</code>拼接客户端线程<code>ID</code>组成，严格来说应该是获取锁的客户端线程唯一标识；</li>
<li><code>VALUE</code>代表的是客户端线程对于锁的持有量。</li>
</ul>
<h2 id="解锁整体流程"><a href="#解锁整体流程" class="headerlink" title="解锁整体流程"></a>解锁整体流程</h2><h3 id="解锁"><a href="#解锁" class="headerlink" title="解锁"></a>解锁</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> RFuture&lt;Void&gt; <span class="title function_">unlockAsync</span><span class="params">(<span class="type">long</span> threadId)</span> &#123;</span><br><span class="line">    RPromise&lt;Void&gt; result = <span class="keyword">new</span> <span class="title class_">RedissonPromise</span>&lt;&gt;();</span><br><span class="line">    <span class="comment">// // 返回的RFuture如果持有的结果为true，说明解锁成功，返回NULL说明线程ID异常，加锁和解锁的客户端线程不是同一个线程</span></span><br><span class="line">    RFuture&lt;Boolean&gt; future = unlockInnerAsync(threadId);</span><br><span class="line">    future.onComplete((opStatus, e) -&gt; &#123;</span><br><span class="line">      	<span class="comment">// 需要取消看门狗的续期任务</span></span><br><span class="line">        cancelExpirationRenewal(threadId);</span><br><span class="line">		<span class="comment">// 这是内部的异常，说明解锁异常</span></span><br><span class="line">        <span class="keyword">if</span> (e != <span class="literal">null</span>) &#123;</span><br><span class="line">            result.tryFailure(e);</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">		<span class="comment">// 这种情况说明线程ID异常，加锁和解锁的客户端线程不是同一个线程，抛出IllegalMonitorStateException异常</span></span><br><span class="line">        <span class="keyword">if</span> (opStatus == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="type">IllegalMonitorStateException</span> <span class="variable">cause</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">IllegalMonitorStateException</span>(<span class="string">&quot;attempt to unlock lock, not locked by current thread by node id: &quot;</span></span><br><span class="line">                    + id + <span class="string">&quot; thread-id: &quot;</span> + threadId);</span><br><span class="line">            result.tryFailure(cause);</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">		<span class="comment">// 走到这里说明正常解锁</span></span><br><span class="line">        result.trySuccess(<span class="literal">null</span>);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<h3 id="解锁的底层逻辑"><a href="#解锁的底层逻辑" class="headerlink" title="解锁的底层逻辑"></a>解锁的底层逻辑</h3><p>核心还是lua脚本实现的原子性</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">protected</span> RFuture&lt;Boolean&gt; <span class="title function_">unlockInnerAsync</span><span class="params">(<span class="type">long</span> threadId)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,</span><br><span class="line">            <span class="comment">// KEYS[1] 锁名</span></span><br><span class="line">            <span class="comment">// ARGV[3] 线程id</span></span><br><span class="line">            <span class="comment">// 如果释放锁的线程和已存在锁的线程不是同一个线程，返回null         </span></span><br><span class="line">            <span class="string">&quot;if (redis.call(&#x27;hexists&#x27;, KEYS[1], ARGV[3]) == 0) then &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;return nil;&quot;</span> +</span><br><span class="line">                    <span class="string">&quot;end; &quot;</span> +</span><br><span class="line">                    <span class="comment">// 通过hincrby递减1的方式，释放一次锁</span></span><br><span class="line">                    <span class="string">&quot;local counter = redis.call(&#x27;hincrby&#x27;, KEYS[1], ARGV[3], -1); &quot;</span> +</span><br><span class="line">                    <span class="comment">// 若剩余次数大于0 ，则刷新过期时间</span></span><br><span class="line">                    <span class="string">&quot;if (counter &gt; 0) then &quot;</span> +</span><br><span class="line">                    <span class="comment">// ARGV[2] 过期时间</span></span><br><span class="line">                    <span class="string">&quot;redis.call(&#x27;pexpire&#x27;, KEYS[1], ARGV[2]); &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;return 0; &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;else &quot;</span> +</span><br><span class="line">                    <span class="comment">// 否则证明锁已经释放，删除key并发布锁释放的消息</span></span><br><span class="line">                    <span class="string">&quot;redis.call(&#x27;del&#x27;, KEYS[1]); &quot;</span> +</span><br><span class="line">                    <span class="comment">// KEYS[2] ： 指的是 publish 的频道名</span></span><br><span class="line">                    <span class="comment">// ARGV[1] ：解锁的消息</span></span><br><span class="line">                    <span class="string">&quot;redis.call(&#x27;publish&#x27;, KEYS[2], ARGV[1]); &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;return 1; &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;end; &quot;</span> +</span><br><span class="line">                    <span class="string">&quot;return nil;&quot;</span>,</span><br><span class="line">            Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<ol>
<li><p><strong>如果解锁的线程和当前锁的线程不是同一个，解锁失败，抛出异常</strong>‘’</p>
</li>
<li><p><strong>通过hincrby递减1，先释放一次锁。</strong></p>
</li>
<li><p><strong>若剩余次数还大于0，则证明当前锁是重入锁，刷新过期时间；</strong></p>
</li>
<li><p><strong>若剩余次数小于0，删除key并发布锁释放的消息，解锁成功</strong></p>
</li>
</ol>
<img src="unlock.webp" style="zoom:60%;" />






<h2 id="Redisson的锁总结"><a href="#Redisson的锁总结" class="headerlink" title="Redisson的锁总结"></a>Redisson的锁总结</h2><p><code>Redisson</code>中的<code>red lock</code>实现，应用到下面的核心技术：</p>
<ul>
<li>合理应用<code>Redis</code>的基本数据类型<code>HASH</code></li>
<li><code>Redis</code>的订阅发布</li>
<li><code>Lua</code>脚本的原子性</li>
<li><code>Netty</code>中的<code>Promise</code>实现</li>
<li><code>Netty</code>中的时间轮<code>HashedWheelTimer</code>和对应的定时任务<code>(HashedWheel)Timeout</code></li>
<li><code>Semaphore</code>进行带期限、永久或者可中断的阻塞以及唤醒，替代<code>CountDownLatch</code>中的无等待期限阻塞</li>
</ul>
<p>上面的核心技术相对合理地应用，才能实现一个高效而且容错能力相对比较高的分布式锁方案，但是从目前来看，<code>Redisson</code>仍未解决<code>red lock</code>算法中的故障转移缺陷，笔者认为这个有可能是<code>Redis</code>实现分布式锁方案的一个底层缺陷，<strong>此方案在<code>Redis</code>单实例中是相对完善</strong>，一旦应用在<code>Redis</code>集群（普通主从、哨兵或者<code>Cluster</code>），有几率会出现前文提到的节点角色切换导致多个不同客户端获取到同一个资源对应的锁的问题。暂时无解。</p>
<blockquote>
<p>但值得注意的是，上面的两种实现方式都是针对单机Redis实例而进行的。如果我们有多个Redis实例，请参阅<strong>Redlock算法</strong>。该算法的具体内容，请参考<a target="_blank" rel="noopener" href="http://redis.cn/topics/distlock.html">http://redis.cn/topics/distlock.html</a></p>
</blockquote>
<blockquote>
<p><a target="_blank" rel="noopener" href="https://www.cnblogs.com/throwable/p/14264804.html">冷饭新炒：理解Redisson中分布式锁的实现 </a></p>
<p><a target="_blank" rel="noopener" href="https://redis.io/topics/distlock">Distributed locks with Redis</a></p>
<p><a target="_blank" rel="noopener" href="https://blog.csdn.net/dlf123321/article/details/123032170?spm=1001.2014.3001.5501">分布式锁的进化过程</a></p>
</blockquote>

    </div>

    
    
    

    <footer class="post-footer">
          <div class="post-tags">
              <a href="/tags/NoSQL/" rel="tag"># NoSQL</a>
              <a href="/tags/Redis/" rel="tag"># Redis</a>
              <a href="/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/" rel="tag"># 分布式锁</a>
          </div>

        

          <div class="post-nav">
            <div class="post-nav-item">
                <a href="/2021/11/29/Jmeter%E4%BD%BF%E7%94%A8/" rel="prev" title="Jmeter使用">
                  <i class="fa fa-chevron-left"></i> Jmeter使用
                </a>
            </div>
            <div class="post-nav-item">
                <a href="/2021/11/29/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%B1%87%E6%80%BB/" rel="next" title="计算机网络面试问题汇总">
                  计算机网络面试问题汇总 <i class="fa fa-chevron-right"></i>
                </a>
            </div>
          </div>
    </footer>
  </article>
</div>






</div>
  </main>

  <footer class="footer">
    <div class="footer-inner">


<div class="copyright">
  &copy; 
  <span itemprop="copyrightYear">2023</span>
  <span class="with-love">
    <i class="fa fa-heart"></i>
  </span>
  <span class="author" itemprop="copyrightHolder">SongyangJi</span>
</div>
  <div class="powered-by">由 <a href="https://hexo.io/" rel="noopener" target="_blank">Hexo</a> & <a href="https://theme-next.js.org/muse/" rel="noopener" target="_blank">NexT.Muse</a> 强力驱动
  </div>

    </div>
  </footer>

  
  <div class="toggle sidebar-toggle" role="button">
    <span class="toggle-line"></span>
    <span class="toggle-line"></span>
    <span class="toggle-line"></span>
  </div>
  <div class="sidebar-dimmer"></div>
  <div class="back-to-top" role="button" aria-label="返回顶部">
    <i class="fa fa-arrow-up fa-lg"></i>
    <span>0%</span>
  </div>

<noscript>
  <div class="noscript-warning">Theme NexT works best with JavaScript enabled</div>
</noscript>


  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js" integrity="sha256-XL2inqUJaslATFnHdJOi9GfQ60on8Wx1C2H8DYiN1xY=" crossorigin="anonymous"></script>
<script src="/js/comments.js"></script><script src="/js/utils.js"></script><script src="/js/motion.js"></script><script src="/js/schemes/muse.js"></script><script src="/js/next-boot.js"></script>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/hexo-generator-searchdb/1.4.1/search.js" integrity="sha256-1kfA5uHPf65M5cphT2dvymhkuyHPQp5A53EGZOnOLmc=" crossorigin="anonymous"></script>
<script src="/js/third-party/search/local-search.js"></script>





  





</body>
</html>
